/*
 * Copyright 2010-2013, David George, Licensed under the Apache License,
 * Version 2.0 (the "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.magneato.mvc;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.magneato.dto.MagUser;
import org.magneato.service.TransactionLogger;
import org.magneato.service.UserManager;
import org.magneato.utils.Authorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.security.authentication.encoding.Md5PasswordEncoder;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.velocity.VelocityEngineUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Validator;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.view.velocity.VelocityConfigurer;

/**
 * This class is the FrontController for all operations concerning user
 * registration and management. It is implemented using the Spring
 * MultiActionController. Different requests are mapped onto corresponding
 * methods with query string parameters mapped onto a WebPage command object.
 * 
 * A set of Velocity templates are used to display the results.
 * 
 * Note the underlying user database (LDAP, Database, file) is not specified.
 * 
 * @author David George
 */
@Controller
public class RegistrationController {
	@Value("${subject}")
	private String subject;
	@Value("${welcome}")
	private String welcome;
	@Value("${confirmation}")
	private boolean confirm;
	@Value("${defaultGroup}")
	private String defaultGroup;
	@Value("${registrationAllowed}")
	private boolean registrationAllowed;
	@Value("${fromEmail}")
	private String adminEmail;

	@Autowired
	private MessageSource messages;

	@Autowired
	private UserManager userManager;

	@Autowired
	private JavaMailSender mailSender;

	@Autowired
	private VelocityConfigurer velocityConfigurer;

	@Autowired
	private TransactionLogger transLogger;

	@Resource(name = "registrationValidators")
	Validator validator;

	private final Log _logger = LogFactory.getLog(RegistrationController.class);

	// TODO: Inject this
	PasswordEncoder encoder = new Md5PasswordEncoder();

	/**
	 * Also bind IP address to command object
	 */
	@InitBinder("user")
	protected void initBinder(HttpServletRequest request,
			ServletRequestDataBinder binder) {

		binder.setValidator(validator);
		MagUser user = (MagUser) binder.getTarget();
		user.setIp(request.getRemoteAddr());
	}

	/**
	 * See whether a user is already registered.
	 * 
	 * @param username
	 * @return "true" if the user is not found, "false" otherwise
	 */
	@RequestMapping(value = "/checkUser.do", method = RequestMethod.GET)
	public ModelAndView checkUser(String username) {
		/*
		 * Pattern p = Pattern.compile("[<>%\$]"); Matcher m =
		 * p.matcher(unsafeInputString); if (m.matches()) { // Invalid input:
		 * reject it, or remove/change the offending characters. } else { //
		 * Valid input. }
		 */
		boolean found = false;
		if (username != null) {
			found = userManager.userExists(username);
		}

		return new ModelAndView("sys/booleanReply", "found", !found);
	}

	/**
	 * Check if screen name is already in use
	 * 
	 * @param screenname
	 * @return "true" if the screenname is not found, "false" otherwise
	 */
	@RequestMapping(value = "/checkScreenName.do", method = RequestMethod.GET)
	public ModelAndView checkScreenName(String screenname) {
		boolean found = true;

		if (screenname != null) {
			try {
				userManager.getScreenName(screenname);
			} catch (org.springframework.dao.EmptyResultDataAccessException ex) {
				// empty result set, screenname is not in use
				found = false;
			}
		}

		return new ModelAndView("sys/booleanReply", "found", !found);
	}

	/**
	 * Deliver change password form to registered user
	 * 
	 * @param request
	 * @return
	 */
	@RequestMapping(value = "/changePassword.do", method = RequestMethod.GET)
	public ModelAndView changePasswordForm(HttpServletRequest request) {

		MagUser user = new MagUser();
		user.setUsername(Authorize.getPrincipal());
		return new ModelAndView("sys/changePassword", "user", user);
	}

	/**
	 * If the logged in user's password. If the user is not logged in this with
	 * throw an Authentication Exception
	 * 
	 * @param request
	 * @param user
	 * @return
	 */
	@RequestMapping(value = "/changePassword.do", method = RequestMethod.POST)
	public ModelAndView changePassword(@ModelAttribute("user") MagUser user,
			HttpServletRequest request) {

		userManager.changePassword(
				encoder.encodePassword(user.getOldPassword(), null),
				encoder.encodePassword(user.getPassword(), null));

		// ok let's email user
		String userName = Authorize.getPrincipal();
		MagUser userDetails = (MagUser) userManager
				.loadUserByUsername(userName);

		SimpleMailMessage message = new SimpleMailMessage();
		message.setTo(userDetails.getUsername());
		message.setFrom(adminEmail);
		message.setSubject("Password Change");

		message.setText("Your password has changed to " + user.getPassword());
		mailSender.send(message);

		return new ModelAndView("sys/infoMsg", "message", messages.getMessage(
				"password.change", null, "",
				RequestContextUtils.getLocale(request)));
	}

	/**
	 * @return reset password form
	 */
	@RequestMapping(value = "/generateReset.do", method = RequestMethod.GET)
	public ModelAndView resetPasswordForm() {
		return new ModelAndView("sys/resetPassword");
	}

	/**
	 * Sends a reset email to the user's email address. This is only the first
	 * stage of the reset process which is: <br/>
	 * ask for reset -> submit email -> generate hash link -> reset password and
	 * send new email Ask for email address need to check email address is valid
	 * and user is enabled.
	 * 
	 * @return
	 */
	@RequestMapping(value = "/generateReset.do", method = RequestMethod.POST)
	public ModelAndView generateReset(String email, HttpServletRequest request) {
		if (email != null && !email.isEmpty()) {
			// catch username not found exception here
			try {
				MagUser user = (MagUser) userManager.loadUserByUsername(email);
				if (!user.isEnabled()) {
					return new ModelAndView("sys/errorMsg", "error",
							"User is not enabled, contact the system administrator");
				}

				String date = user.getJoinDateString();
				String screenname = user.getScreenname();
				// generate hash, a hacker won't know the screenname or be able
				// to predict the join date
				String hash = encoder.encodePassword(date + screenname, "");

				String url = "http://" + request.getServerName() + ":"
						+ request.getServerPort() + request.getContextPath();

				SimpleMailMessage message = new SimpleMailMessage();
				message.setTo(user.getUsername());
				message.setFrom("noreply@mail.com");
				message.setSubject("Password reset");

				Map<String, Object> model = new HashMap<String, Object>();
				model.put("hash", hash);
				model.put("email", email);
				model.put("url", url);

				String text = VelocityEngineUtils.mergeTemplateIntoString(
						velocityConfigurer.getVelocityEngine(),
						"sys/resetMsg.vm", model);

				message.setText(text);

				mailSender.send(message);
			} catch (Exception e) {
				return new ModelAndView("sys/resetPassword", "error",
						"Unknown email address");
			}
		}

		return new ModelAndView("sys/infoMsg", "message",
				"You will shortly receive an email with details of how to reset your password.");
	}

	/**
	 * Needs an email and generated hash id.
	 * 
	 * @return
	 */
	@RequestMapping(value = "/resetPassword.do", method = RequestMethod.GET)
	public ModelAndView resetPassword(String id, String email,
			HttpServletRequest request) {
		String ipAddr = request.getRemoteAddr();
		_logger.info("Password reset sent for " + email + " remote IP address "
				+ ipAddr);

		if (email != null && !email.isEmpty()) {
			// catch username not found exception here
			try {
				MagUser user = (MagUser) userManager.loadUserByUsername(email);
				if (!user.isEnabled()) {
					return new ModelAndView("sys/errorMsg", "error",
							"User is not enabled, contact the system administrator");

				}

				String date = user.getJoinDateString();
				String screenname = user.getScreenname();
				// generate hash, a hacker won't know the screenname or be able
				// to predict the join date
				String hash = encoder.encodePassword(date + screenname, "");
				if (!hash.equals(id)) {
					return new ModelAndView(
							"sys/errorMsg",
							"error",
							"The link you have clicked on is invalid, please contact a system administrator");
				}

				String password = RandomStringUtils.randomAlphanumeric(12);
				user.setPassword(encoder.encodePassword(password, null));
				userManager.updateUser(user);

				SimpleMailMessage message = new SimpleMailMessage();
				message.setTo(user.getUsername());
				message.setFrom("noreply@mail.com");
				message.setSubject("You got mail!");

				Map<String, Object> model = new HashMap<String, Object>();
				model.put("password", password);
				model.put("ipAddr", ipAddr);

				String text = VelocityEngineUtils.mergeTemplateIntoString(
						velocityConfigurer.getVelocityEngine(),
						"sys/passwordReset.vm", model);

				message.setText(text);

				mailSender.send(message);

			} catch (Exception e) {
				e.printStackTrace();
				return new ModelAndView(
						"sys/errorMsg",
						"error",
						"The link you have clicked on is invalid, please contact a system administrator");
			}
		}

		String message = messages.getMessage("password.reset", null, "",
				RequestContextUtils.getLocale(request));
		return new ModelAndView("sys/infoMsg", "message", message);
	}

	/**
	 * Does nothing more than return a registration form to the end user with an
	 * command object containing only the referring page (so we can redirect
	 * later).
	 * 
	 * @param request
	 *            Servlet request object
	 * @param response
	 *            Servlet response object
	 * @return registration form view
	 */

	@RequestMapping(value = "/register.do", method = RequestMethod.GET)
	public ModelAndView regUser(HttpServletRequest request) {
		if (registrationAllowed) {
		MagUser user = new MagUser();
		user.setReferer(request.getHeader("Referer"));
		user.setScreenname("");
		return new ModelAndView("sys/register", "user", user);
		} else {
			return new ModelAndView("sys/noregistration", "referrer",
					request.getParameter("referer"));
		}
	}

	/**
	 * <ul>
	 * <li>Inserts user details into store
	 * <li>Email's user with registration activation details, registerUser
	 * should return an activation code
	 * </ul>
	 * 
	 * @param request
	 * @param user
	 * @return
	 */
	@RequestMapping(value = "/register.do", method = RequestMethod.POST)
	public ModelAndView createUser(@ModelAttribute("user") @Valid MagUser user,
			BindingResult result, HttpServletRequest request) {

		if (!registrationAllowed) {
			return new ModelAndView("sys/noregistration", "referrer",
					request.getParameter("referer"));
		}

		if (result.hasErrors()) {
			return new ModelAndView("sys/register");
		}

		user.setJoinDate(System.currentTimeMillis());
		user.setIp(request.getRemoteAddr());
		addUser(user);

		if (confirm) {
			String date = user.getJoinDateString();
			String screenname = user.getScreenname();
			// generate hash, a hacker won't know the screenname or be able
			// to predict the join date
			String hash = encoder.encodePassword(date + screenname, "");
			// userManager.addUserToGroup(user.getUsername(), "Editors");

			String activation = encoder.encodePassword(user.getUsername(),
					"salt");
			StringBuilder message = new StringBuilder();

			message.append(welcome);
			// build URL
			message.append("http://");
			message.append(request.getServerName());
			message.append(":");
			message.append(request.getServerPort());
			message.append(request.getContextPath());
			message.append("/confirmation.do");
			message.append("?id=" + hash + "&username=" + user.getUsername());
			if (activation != null) {
				
				SimpleMailMessage msg = new SimpleMailMessage();
				msg.setTo(user.getUsername());
				msg.setFrom(adminEmail);
				msg.setSubject(subject);
				msg.setText(message.toString());
				mailSender.send(msg);
			}

		}
		transLogger.logRegistration(user);
		return new ModelAndView("sys/registerSucess", "command", user);
	}

	/**
	 * User
	 * 
	 * @param user
	 * @return
	 */
	@RequestMapping(value = "/confirmation.do", method = RequestMethod.GET)
	public ModelAndView confirmation(String username, String id) {
		MagUser user = (MagUser) userManager.loadUserByUsername(username);
		String date = user.getJoinDateString();
		String screenname = user.getScreenname();
		// generate hash, a hacker won't know the screenname or be able
		// to predict the join date
		String hash = encoder.encodePassword(date + screenname, "");
		if (!hash.equals(id)) {
			return new ModelAndView(
					"sys/errorMsg",
					"error",
					"The link you have clicked on is invalid, please contact a system administrator");
		}
		user.setEnabled(true);
		userManager.updateUser(user);

		return new ModelAndView("sys/infoMsg", "message",
				"your account is now enabled");
	}

	/**
	 * TODO: what happens to a user's pages?
	 * 
	 * @param userName
	 * @return
	 */
	@RequestMapping(value = "/deleteUser.do", method = RequestMethod.GET)
	public ModelAndView deleteUser(String userName) {
		userManager.deleteUser(userName);
		return new ModelAndView("sys/registerSucess", "command", null);
	}

	/**
	 * 
	 * username=bill%40domain.com&screenname=davidoff&joindate=12+Feb+2013&
	 * ip_addr
	 * =0%3A0%3A0%3A0%3A0%3A0%3A0%3A1&enabled=Yes&password=*****&oper=edit&id=1
	 * 
	 * 
	 * @param oper
	 *            : add/delete/update
	 * @param userName
	 * @param oper
	 * @param email
	 * @return needs admin role to add or delete
	 * @throws ParseException
	 */
	@RequestMapping(value = "/updateUser.do", method = RequestMethod.POST)
	public ModelAndView updateUser(String oper, String id, String screenname,
			String password, String enabled, String joindate, String ip_addr,
			String username) {

		if ("del".equals(oper)) {
			int iD = Integer.parseInt(id);
			MagUser user = userManager.loadUserById(iD);
			userManager.deleteUser(user.getUsername());
			return new ModelAndView("sys/registerSucess");
		} else if ("add".equals(oper)) {
			MagUser user = new MagUser();
			user.setUsername(username);
			user.setJoinDate(System.currentTimeMillis());
			if ("Yes".equals(enabled)) {
				user.setEnabled(true);
			} else {
				user.setEnabled(false);
			}
			user.setScreenname(screenname);
			user.setIp(ip_addr);
			ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
			grantedAuthorities.add(new SimpleGrantedAuthority(defaultGroup));
			user.setAuthorities(grantedAuthorities);
			addUser(user);
		} else if ("edit".equals(oper)) {
			MagUser user = (MagUser) userManager.loadUserByUsername(username);

			if ("Yes".equals(enabled)) {
				user.setEnabled(true);
			} else {
				user.setEnabled(false);
			}
			user.setIp(ip_addr);
			user.setScreenname(screenname);
			if (!"*****".equals(password)) {
				user.setPassword(encoder.encodePassword(password, null));
			} else {
				user.setPassword(null);
			}
			SimpleDateFormat formatter = new SimpleDateFormat(
					MagUser.dateFormat);
			try {
				user.setJoinDate(formatter.parse(joindate).getTime());
			} catch (ParseException ex) {
				// TODO logging
			}

			userManager.updateUser(user);

			return new ModelAndView("sys/registerSucess", "command", user);
		}
		return new ModelAndView("sys/registerSucess");

	}

	/**
	 * @param search
	 *            - true if searching, false otherwise
	 * 
	 * @param page
	 *            - by default 1
	 * @param rows
	 *            - no of rows to fetch
	 * @param sidx
	 *            - Row name to index on
	 * @param sord
	 *            - sort order
	 * 
	 * @param searchField
	 *            field to search on, if searching
	 * @param searchString
	 *            string to search on
	 * @param searchOper
	 *            eq - equals, bw - begins with
	 * @param filters
	 * 
	 *            TODO: add order by and searchOper
	 */

	@RequestMapping(value = "/listUsers.do", method = RequestMethod.POST)
	public ModelAndView listUsers(boolean _search, int page, int rows,
			String sidx, String sord, String searchField, String searchString,
			String searchOper) {

		int userCount = userManager.getUserCount();
		int start = (page - 1) * rows;

		List<UserDetails> users = null;
		if (_search) {
			users = userManager.listAllUsers(start, start + rows, searchField,
					searchString, searchOper);
		} else {
			users = userManager.listAllUsers(start, start + rows);
		}

		ModelAndView view = new ModelAndView("sys/listUsers", "users", users);
		view.addObject("userCount", userCount);
		view.addObject("page", page);

		int pages = (int) Math.ceil((double) userCount / rows);

		view.addObject("pages", pages);
		return view;
	}

	@RequestMapping(value = "/listRoles.do", method = RequestMethod.POST)
	public ModelAndView listGroups(int id) {
		List<String> groups = userManager.listUserGroup(id);
		return new ModelAndView("sys/listGroups", "groups", groups);
	}

	@RequestMapping(value = "/updateGroup.do", method = RequestMethod.POST)
	public ModelAndView updateUser(String oper, String id, int userId,
			String group) {
		MagUser user = null;
		try {

			user = userManager.loadUserById(userId);

			Collection<? extends GrantedAuthority> grantedAuthorities = user
					.getAuthorities();
			ArrayList<GrantedAuthority> newGA = new ArrayList<GrantedAuthority>();
			if ("del".equals(oper)) {
				Iterator<? extends GrantedAuthority> iter = grantedAuthorities
						.iterator();
				while (iter.hasNext()) {
					GrantedAuthority authority = iter.next();
					if (!authority.getAuthority().equals(id)) {
						newGA.add(authority);
					}
				}

			} else if ("add".equals(oper)) {
				Iterator<? extends GrantedAuthority> iter = grantedAuthorities
						.iterator();
				while (iter.hasNext()) {
					GrantedAuthority authority = iter.next();
					newGA.add(authority);
				}
				newGA.add(new SimpleGrantedAuthority(group));

			}
			user.setAuthorities(newGA);
			userManager.updateUser(user);
		} catch (UsernameNotFoundException e) {
			_logger.debug(e.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}

		return new ModelAndView("sys/registerSucess", "command", user);
	}

	/**
	 * Format of import is:- username,screename,ip_address,join_date,password
	 * 
	 * @return
	 */
	@RequestMapping(value = "/importUsers.do", method = RequestMethod.GET)
	public ModelAndView importUsers() {

		InputStream is = null;

		try {
			is = new FileInputStream("users.csv");
		} catch (FileNotFoundException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
			return new ModelAndView("sys/admin", "message",
					"No user file found");

		}
		Scanner scanner = new Scanner(is);
		int count = 1;
		while (scanner.hasNextLine()) {
			MagUser user = new MagUser();
			try {
				String curLine = scanner.nextLine().trim();
				Scanner lineScanner = new Scanner(curLine);
				lineScanner.useDelimiter(",");

				user.setScreenname(lineScanner.next());
				user.setUsername(lineScanner.next());
				user.setPassword(lineScanner.next());
				user.setIp(lineScanner.next());
				long creatDate = Long.parseLong(lineScanner.next()) * (1000);
				user.setJoinDate(creatDate);
				user.setEnabled(true);

				ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
				grantedAuthorities
						.add(new SimpleGrantedAuthority(defaultGroup));
				user.setAuthorities(grantedAuthorities);

				userManager.createUser(user);
				count++;
			} catch (Exception e1) {
				// TODO Auto-generated catch block
				_logger.debug("couldn't import user " + user.toString());
			}
		}
		scanner.close();

		try {
			if (is != null) {
				is.close();
			}
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

		return new ModelAndView("sys/admin", "message", "Imported " + count
				+ " users");
	}

	@RequestMapping(value = "/getCredentials.do", method = RequestMethod.GET)
	public ModelAndView getCredentials() {
		return new ModelAndView("sys/userid");
	}

	private void addUser(MagUser user) {
		user.setPassword(encoder.encodePassword(user.getPassword(), null));

		ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<GrantedAuthority>();
		grantedAuthorities.add(new SimpleGrantedAuthority(defaultGroup));
		user.setAuthorities(grantedAuthorities);
		if (!confirm) {
			user.setEnabled(true);
		}
		userManager.createUser(user);
	}
}
